﻿using System;
using OpenGLES;
using System.IO;
using System.Windows.Forms;
using System.Drawing;
using System.Collections.Generic;

namespace PFD
{
    /// <summary>
    /// class RenderDeviceOGL derives from IRenderDevice. 
    /// Implements the render device using OpenGLES.
    /// </summary>
    public class RenderDeviceOGL : IRenderDevice
    {
        bool bInitialized = false;

        EGLDisplay myDisplay;
        EGLSurface mySurface;
        EGLContext myContext;

        OpenGLFont myFont;

        /// <summary>
        /// Maintain a list of textures
        /// </summary>
        List<RenderTextureOGL> textures = new List<RenderTextureOGL>();

        /// <summary>
        /// Maintain a list of vertex buffers
        /// </summary>
        List<VertexBufferOGL> vertexBuffers = new List<VertexBufferOGL>();

        /// <summary>
        /// Maintain a flag to indicate whether the object has already been disposed or not
        /// </summary>
        bool Disposed = false;

        /// <summary>
        /// The variable SinglePassMultiTextureEnabled determines whether or not the
        /// graphics devices supports multitexturing using a single pass. If not, the
        /// multiple textures have to be rendered in multiple passes.
        /// </summary>
        bool SinglePassMultiTextureEnabled = false;

        /// <summary>
        /// Constructor
        /// </summary>
        public RenderDeviceOGL()
        {
            bInitialized = false;
        }

        /// <summary>
        /// Initialize - function initializes the object.
        /// </summary>
        /// <param name="mnForm">The form to which to render content</param>
        /// <returns>true, if initializing succeeded</returns>
        unsafe public bool Initialize(Form mnForm)
        {
            if (bInitialized)
                return true; // Already initialized

            myDisplay = egl.GetDisplay(new EGLNativeDisplayType(mnForm));

            int major, minor;
            egl.Initialize(myDisplay, out major, out minor);

            EGLConfig[] configs = new EGLConfig[10];
            int[] attribList = new int[] 
            { 
                egl.EGL_RED_SIZE, 5, 
                egl.EGL_GREEN_SIZE, 6, 
                egl.EGL_BLUE_SIZE, 5, 
                egl.EGL_DEPTH_SIZE, 16 , 
                egl.EGL_SURFACE_TYPE, egl.EGL_WINDOW_BIT,
                egl.EGL_STENCIL_SIZE, egl.EGL_DONT_CARE,
                egl.EGL_NONE, egl.EGL_NONE 
            };

            int numConfig;
            if (!egl.ChooseConfig(myDisplay, attribList, configs, configs.Length, out numConfig) || numConfig < 1)
                throw new InvalidOperationException("Unable to choose config.");

            EGLConfig config = configs[0];
            mySurface = egl.CreateWindowSurface(myDisplay, config, mnForm.Handle, null);
            myContext = egl.CreateContext(myDisplay, config, EGLContext.None, null);

            if (!egl.MakeCurrent(myDisplay, mySurface, mySurface, myContext))
            {
                //  On my HD2 this happens after every 3rd run of the program.
                //  There must be an issue somewhere (forgot to release something somewhere)
                MessageBox.Show("Could not make rending context current ! Please try to restart your device.");
                return false;
            }

            // Enable flat shading per default and set clear color to black
            EnableShading(PFD.SHADE_MODE.Flat);
            gl.ClearColor(0.5f, 0.5f, 0.0f, 0.5f);
            gl.ClearDepthf(1.0f);
            //  Enable depth testing
            EnableDepthTest(true);

            //  Blending is enabled per default (do not know if this makes sense)
            gl.Enable(gl.GL_BLEND);
            gl.BlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA);
            gl.DepthFunc(gl.GL_LEQUAL);
            gl.Hint(gl.GL_PERSPECTIVE_CORRECTION_HINT, gl.GL_NICEST);

            myFont = new OpenGLFont(new System.Drawing.Font(FontFamily.GenericSerif, 12, FontStyle.Regular));

            //  Detemine if the device is capable of rendering multiple textures in a single pass
            //  OpenGL will be queried for the maximum number of texture units
            int[] nNumTextureStages = new int[1];

            fixed (int* p = &nNumTextureStages[0])
                gl.GetIntegerv(gl.GL_MAX_TEXTURE_UNITS, p);
            uint nErrr = gl.GetError();
            if (nNumTextureStages[0] > 1)
                SinglePassMultiTextureEnabled = true;
            else
                SinglePassMultiTextureEnabled = false;

            SinglePassMultiTextureEnabled = false;
            return true;
        }

        /// <summary>
        /// Function SetViewport sets the viewport using the given Rectangle
        /// </summary>
        /// <param name="client">the client rectangle to be used as viewport</param>
        /// <returns>true, in case of no error</returns>
        public bool SetViewport(Rectangle client)
        {
            gl.Viewport(client.Left, client.Top, client.Width, client.Height);
            if (CheckError("Setting Viewport using SetViewport function"))
                return false;
            return true;
        }

        /// <summary>
        /// Function SetOrthographicProjecttion
        /// </summary>
        /// <param name="l">left</param>
        /// <param name="r">right</param>
        /// <param name="t">top</param>
        /// <param name="b">bottom</param>
        /// <param name="n">near clipping plane</param>
        /// <param name="f">far clipping plane</param>
        /// <returns>true, if successful = always</returns>
        public bool SetOrthographicProjection(float l, float r, float t, float b, float n, float f)
        {
            gl.MatrixMode(gl.GL_PROJECTION);
            gl.LoadIdentity();
            gl.Orthof(l, r, b, t, n, f);
            return true;
        }

        /// <summary>
        /// Function EnableTexturing
        /// </summary>
        /// <param name="bEnable">specifies whether to enable (true) or disable (false) texturing</param>
        /// <returns>true, if successful (always)</returns>
        public bool EnableTexturing(bool bEnable)
        {
            if (bEnable)
                gl.Enable(gl.GL_TEXTURE_2D);
            else
                gl.Disable(gl.GL_TEXTURE_2D);

            return true;
        }

        /// <summary>
        /// Function EnableShading - Sets the given shading mode
        /// </summary>
        /// <param name="sm">Currently only SHADE_MODE.Flat is possible</param>
        /// <returns>true, if successful, false otherwise</returns>
        public bool EnableShading(PFD.SHADE_MODE sm)
        {
            bool bRetVal = false;

            switch (sm)
            {
                case PFD.SHADE_MODE.Flat:
                    gl.ShadeModel(gl.GL_FLAT);
                    bRetVal = true;
                    break;

                default:
                    bRetVal = false;
                    break;
            }

            return bRetVal;
        }

        /// <summary>
        /// Function EnableDepthTest - Enable or disable the Depth Testing Feature
        /// </summary>
        /// <param name="bEnable">set to true to enable, or false to disable depth testing</param>
        /// <returns>true, if successful (always)</returns>
        public bool EnableDepthTest(bool bEnable)
        {
            if (bEnable)
                gl.Enable(gl.GL_DEPTH_TEST);
            else
                gl.Disable(gl.GL_DEPTH_TEST);

            return true;
        }

        /// <summary>
        /// Function EnableDithering - Enable or disable the dithering feature
        /// </summary>
        /// <param name="bEnable">set to true to enable, or false to disable dithering</param>
        /// <returns>true, if successful (always)</returns>
        public bool EnableDithering(bool bEnable)
        {
            if (bEnable)
                gl.Enable(gl.GL_DITHER);
            else
                gl.Disable(gl.GL_DITHER);

            return true;
        }

        /// <summary>
        /// Function EnableBlending - Enable or disable texture blending feature
        /// </summary>
        /// <param name="bEnable">set to true to enable, or false to disable texture blending</param>
        /// <returns>true, if successful (always)</returns>
        public bool EnableBlending(bool bEnable)
        {
            if (bEnable)
                gl.Enable(gl.GL_BLEND);
            else
                gl.Disable(gl.GL_BLEND);
            return true;
        }

        /// <summary>
        /// Function Clear - clear backbuffer and depthbuffer
        /// </summary>
        /// <param name="bClrColor">set to true to clear colorbuffer</param>
        /// <param name="bClrDepth">set to true to clear depthbuffer</param>
        /// <returns>true, if successful (always)</returns>
        public bool Clear(bool bClrColor, bool bClrDepth)
        {
            uint clrmask = 0;

            if (bClrColor)
                clrmask = gl.GL_COLOR_BUFFER_BIT;
            if (bClrDepth)
                clrmask |= gl.GL_DEPTH_BUFFER_BIT;

            gl.Clear(clrmask);

            return true;
        }

        /// <summary>
        /// Function DrawString - Draw a string at the given 3D position
        /// </summary>
        /// <param name="s">string to draw</param>
        /// <param name="x">x - coordinate for the text</param>
        /// <param name="y">y - coordinate for the text</param>
        /// <param name="z">z - coordinate for the text</param>
        /// <returns>true, if successful (always)</returns>
        public bool DrawString(string s, float x, float y, float z)
        {
            gl.LoadIdentity();
            gl.Translatef(x, y, z);
            gl.Scalef(1.0f, 1.0f, 0f);
            GlyphRun fpsText = new GlyphRun(myFont, s);
            fpsText.Draw();
            return true;
        }

        /// <summary>
        /// Function CreateVertexBuffer - function creates a new vertex buffer object 
        /// sufficiently large to hold the given number of vertices of the given format.
        /// </summary>
        /// <param name="numVertices">Number of vertices that the vertex buffer object shall be able to receive</param>
        /// <param name="vet">Specifies the elements that the vertices will contain</param>
        /// <returns>The created vertex buffer object</returns>
        public IVertexBuffer CreateVertexBuffer(uint numVertices, VertexElementType vet)
        {
            VertexBufferOGL vb = new VertexBufferOGL(numVertices, vet);

            return vb;
        }

        /// <summary>
        /// Function DrawObject - Draws an object using the textures and vertexbuffer provided
        /// </summary>
        /// <param name="dm">Draw mode, can currently be either FAN or STRIP</param>
        /// <param name="ivb">A valid vertex buffer object</param>
        /// <param name="texs">A IRenderTextureProvider derived obejct, which provides the necessary textures
        /// of type IRenderTexture.</param>
        /// <returns>true, if successful</returns>
        public bool DrawObject(DRAW_MODE dm, IVertexBuffer ivb, IRenderTextureProvider texs)
        {
            VertexBufferOGL vbogl = (VertexBufferOGL)ivb;

            uint glDrawMode = gl.GL_TRIANGLE_FAN;

            switch (dm)
            {
                case DRAW_MODE.TriangleFan:
                    glDrawMode = gl.GL_TRIANGLE_FAN;
                    break;

                case DRAW_MODE.TriangleStrip:
                    glDrawMode = gl.GL_TRIANGLE_STRIP;
                    break;

                default:
                    break;
            }

            if (SinglePassMultiTextureEnabled )
            {
                //  Procedure is like follows:
                //  In case the first texture has blending enabled, the global blending will be enabled.
                //  It will be disabled otherwise --> the resulting textured polygon will not be blended
                //  with background.
                //  if the second texture has the blending mode enabled then the texture operation will be
                //  set to blending and set to replace otherwise.
                gl.ActiveTexture(gl.GL_TEXTURE0);
#if DEBUG
                CheckError("Activating Texture0");
#endif
                gl.Enable(gl.GL_TEXTURE_2D);
#if DEBUG
                CheckError("Enableing Texture");
#endif
                //  Set first texture stage to always replace with current texture
                gl.TexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_COMBINE);
                gl.TexEnvf(gl.GL_TEXTURE_ENV, gl.GL_COMBINE_RGB, gl.GL_REPLACE);
                gl.TexEnvf(gl.GL_TEXTURE_ENV, gl.GL_COMBINE_ALPHA, gl.GL_REPLACE);
                gl.TexEnvf(gl.GL_TEXTURE_ENV, gl.GL_SRC0_RGB, gl.GL_TEXTURE);
                gl.TexEnvf(gl.GL_TEXTURE_ENV, gl.GL_OPERAND0_RGB, gl.GL_SRC_COLOR);
                gl.TexEnvf(gl.GL_TEXTURE_ENV, gl.GL_SRC1_RGB, gl.GL_TEXTURE);
                gl.TexEnvf(gl.GL_TEXTURE_ENV, gl.GL_OPERAND1_RGB, gl.GL_SRC_COLOR);
                gl.TexEnvf(gl.GL_TEXTURE_ENV, gl.GL_SRC0_ALPHA, gl.GL_TEXTURE);
                gl.TexEnvf(gl.GL_TEXTURE_ENV, gl.GL_OPERAND0_ALPHA, gl.GL_SRC_ALPHA);
                gl.TexEnvf(gl.GL_TEXTURE_ENV, gl.GL_SRC1_ALPHA, gl.GL_TEXTURE);
                gl.TexEnvf(gl.GL_TEXTURE_ENV, gl.GL_OPERAND1_ALPHA, gl.GL_SRC_ALPHA);


                EnableBlending(texs[0, RenderTextureProviderState.BlendingEnabled]);

                gl.BindTexture(gl.GL_TEXTURE_2D, ((RenderTextureOGL)texs[0]).Name);
#if DEBUG
                CheckError("Binding Texture 0");
#endif
                gl.ActiveTexture(gl.GL_TEXTURE1);
#if DEBUG
                CheckError("Activating Texture1");
#endif
                if (texs.Count > 1)
                {
                    gl.Enable(gl.GL_TEXTURE_2D);
#if DEBUG
                    CheckError("Enableing Texture");
#endif
                    gl.BindTexture(gl.GL_TEXTURE_2D, ((RenderTextureOGL)texs[1]).Name);
/*
                    if (texs[1, RenderTextureProviderState.BlendingEnabled])
                    {
                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_DECAL);
                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_SRC0_RGB, gl.GL_TEXTURE);
                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_OPERAND0_RGB, gl.GL_SRC_COLOR);
                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_SRC1_RGB, gl.GL_PREVIOUS);
                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_OPERAND1_RGB, gl.GL_SRC_COLOR);
                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_SRC0_ALPHA, gl.GL_TEXTURE);
                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_OPERAND0_ALPHA, gl.GL_SRC_ALPHA);
                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_SRC1_ALPHA, gl.GL_PREVIOUS);
                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_OPERAND1_ALPHA, gl.GL_SRC_ALPHA);
                    }
                    else*/
                    {
                        gl.TexEnvf(gl.GL_TEXTURE_ENV, gl.GL_TEXTURE_ENV_MODE, gl.GL_REPLACE);
//                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_SRC0_RGB, gl.GL_TEXTURE);
//                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_OPERAND0_RGB, gl.GL_SRC_COLOR);
//                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_SRC0_ALPHA, gl.GL_TEXTURE);
//                        gl.TexEnvi(gl.GL_TEXTURE_ENV, gl.GL_OPERAND0_ALPHA, gl.GL_SRC_ALPHA);
                    }
                }
                else
                {
                    gl.Disable(gl.GL_TEXTURE_2D);
                }
#if DEBUG
                CheckError("Drawing Vertex Array");
#endif
                //  this procedure is only interesting in case more than one texture is available
                //  we first realize the vertex buffer for both textures
                vbogl.RealizeVertexBuffer(this, true, 0);

                gl.DrawArrays(glDrawMode, (int)0, (int)vbogl.NumberOfVertices);
#if DEBUG
                CheckError("Drawing Vertex Array");
#endif

                gl.ActiveTexture(gl.GL_TEXTURE1);
                gl.Disable(gl.GL_TEXTURE_2D);
                gl.ActiveTexture(gl.GL_TEXTURE1);
                gl.Disable(gl.GL_TEXTURE_2D);
#if DEBUG
                CheckError("Drawing Vertex Array");
#endif
            }
            else
            {
                //  No single pass multi-texturing possible
                //  so that in case two textures shall be drawn, two
                //  consecutive DRAWARRAY calls are necessary with different
                //  vertex sets selected

                for (uint nTex = 0; nTex < texs.Count; nTex++ )
                {
                    RenderTextureOGL tex = (RenderTextureOGL)texs[nTex];
                    vbogl.RealizeVertexBuffer(this, false, nTex);
                    gl.Enable(gl.GL_TEXTURE_2D);

                    gl.BindTexture(gl.GL_TEXTURE_2D, tex.Name);
#if DEBUG
                    CheckError("Binding Texture");
#endif
                    EnableBlending(texs[nTex, RenderTextureProviderState.BlendingEnabled]);

                    gl.DrawArrays(glDrawMode, (int)0, (int)vbogl.NumberOfVertices);
#if DEBUG
                    CheckError("Drawing Vertex Array");
#endif
                }
                gl.DisableClientState(gl.GL_TEXTURE_COORD_ARRAY);
            }
            return true;
        }

        /// <summary>
        /// Function SwapBuffers - Function realizes previous rendering and flips buffers.
        /// </summary>
        /// <returns>true, if successful, false otherwise</returns>
        public bool SwapBuffers()
        {
            gl.Flush();
            if (CheckError("in FLUSH"))
                return false;
            egl.SwapBuffers(myDisplay, mySurface);
            if (CheckError("in SwapBuffers"))
                return false;
            return true;
        }

        /// <summary>
        /// BeginScene function is not required for OpenGL
        /// </summary>
        /// <returns></returns>
        public bool BeginScene()
        {
            return true;
        }

        /// <summary>
        /// EndScene function is not requried for OpenGL
        /// </summary>
        /// <returns></returns>
        public bool EndScene()
        {
            return true;
        }


        /// <summary>
        /// Function ResetObjectMatrix - Set Modelview Matrix to identity matrix
        /// </summary>
        /// <returns>true, if successful (always)</returns>
        public bool ResetObjectMatrix()
        {
            gl.MatrixMode(gl.GL_MODELVIEW);
            gl.LoadIdentity();
            return true;
        }

        /// <summary>
        /// Function TranslateObject - Move Object about the given coordinates
        /// </summary>
        /// <param name="x">delta x</param>
        /// <param name="y">delta y</param>
        /// <param name="z">delta z</param>
        /// <returns>true, if successful (always)</returns>
        public bool TranslateObject(float x, float y, float z)
        {
            gl.Translatef(x, y, z);
            return true;
        }

        /// <summary>
        /// Function RotateObject - Rotate Object about the given axis and angle
        /// </summary>
        /// <param name="angle">delta angle</param>
        /// <param name="x">axis x</param>
        /// <param name="y">axis y</param>
        /// <param name="z">axis z</param>
        /// <returns>true, if successful (always)</returns>
        public bool RotateObject(float angle, float x, float y, float z)
        {
            gl.Rotatef(angle, x, y, z);
            return true;
        }

        /// <summary>
        /// Function ScaleObject - Scale Object about in 3D space
        /// </summary>
        /// <param name="x">x-scale factor</param>
        /// <param name="y">y-scale factor</param>
        /// <param name="z">z-scale factor</param>
        /// <returns>true, if successful (always)</returns>
        public bool ScaleObject(float x, float y, float z)
        {
            gl.Scalef(x, y, z);
            return true;
        }

        /// <summary>
        /// Function CreateTexture - Create a new texture object from file
        /// </summary>
        /// <param name="path">Path to file from which the texture object shall be created</param>
        /// <param name="bIsTransparent">true if transparent texture is required</param>
        /// <returns>New texture object as IRenderTexture</returns>
        public IRenderTexture CreateTexture(string path, bool bIsTransparent)
        {
            //  Please note that the FileStream will throw an exception if the resources
            //  are write protected (so you need to make the resources writeable)
            FileStream s = new FileStream(path, FileMode.Open);

            RenderTextureOGL r = new RenderTextureOGL(s, bIsTransparent);

            return r;
        }

        /// <summary>
        /// Function CheckError - Checks for OpenGL error and throws an exception if an error is reported
        /// </summary>
        /// <param name="msg">Message to display in case of an error</param>
        /// <returns>false if no error is detected</returns>
        private bool CheckError(string msg)
        {
            uint error = gl.GetError();

            if (error == 1285)
            {
                throw new OutOfMemoryException("OpenGL has detected a memory problem");
            }
            else if (error!=0)
            {
                throw new Exception(msg + " OpenGLES Error detected with no : " + error.ToString());
            }

            return false;
        }

        #region IDisposable Members

        /// <summary>
        /// Disposes the object and all related resources, including all objects
        /// created by this objects -> vertex buffers and textures
        /// </summary>
        public void Dispose()
        {
            if (!Disposed)
            {
                foreach (VertexBufferOGL vb in vertexBuffers)
                    vb.Dispose();
                foreach (RenderTextureOGL t in textures)
                    t.Dispose();

                if (!egl.DestroySurface(myDisplay, mySurface))
                    throw new Exception("Error while destroying surface.");
                if (!egl.DestroyContext(myDisplay, myContext))
                    throw new Exception("Error while destroying context.");
                if (!egl.Terminate(myDisplay))
                    throw new Exception("Error while terminating display.");
            }
        }

        #endregion

        #region IRenderDevice Members

        #endregion
    }
}
